Qutebrowser 如何通过程序选择页面元素
我热衷于对 qutebrowser 进行二次开发。在本文中,探讨一个核心操作,如何选择页面中的元素。
从 Tab 说起
在 qutebrowser 中,每个 Tab 的类型为 AbstractTab,其内部有一个核心成员:
elements: AbstractElements
elements
表示所有的页面元素。对页面元素的访问问题,就是对 elements
的访问问题。
AbstractTab 中的 elements
AbstractTab 是抽象类,之包含了 elements 的声明,不包含赋值。赋值操作在子类中完成。AbstractTab 有两个子类 WebEngineTab 和 WebKitTab。这里以 WebEngineTab 为例,创建实例:
self.elements = WebEngineElements(tab=self)
WebEngineElements 的 find 系列方法
在 WebEngineElements 中,定义有一系列 find 方法:
find_css
:CSS 选择find_id
:id 选择find_focused
:聚焦选择find_at_pos
:位置选择
find_css
这里以 find_css 为例。
Python 侧实现
首先先看基类 AbstractElements 中的声明:
_MultiCallback = Callable[[Sequence['webelem.AbstractWebElement']], None]
def find_css(self, selector: str,
callback: _MultiCallback,
error_cb: _ErrorCallback, *,
only_visible: bool = False) -> None:
"""Find all HTML elements matching a given selector async.
If there's an error, the callback is called with a webelem.Error
instance.
Args:
callback: The callback to be called when the search finished.
error_cb: The callback to be called when an error occurred.
selector: The CSS selector to search for.
only_visible: Only show elements which are visible on screen.
"""
raise NotImplementedError
其中:通过 callback 回调返回页面元素。并且类型是 AbstractWebElement 序列。
再看 QWebEngineTab 的实现:
def find_css(self, selector, callback, error_cb, *,
only_visible=False):
js_code = javascript.assemble('webelem', 'find_css', selector,
only_visible)
js_cb = functools.partial(self._js_cb_multiple, callback, error_cb)
self._tab.run_js_async(js_code, js_cb)
可见,页面元素搜索,实际上是执行了一段 JavaScript。
JavaScript 侧实现
而这段 JavaScript,是 qutebrowser 提前预埋进去的。位于 webelem.js
:
funcs.find_css = (selector, only_visible) => {
let elems;
try {
elems = document.querySelectorAll(selector);
} catch (ex) {
return {"success": false, "error": ex.toString()};
}
const subelem_frames = window.frames;
const out = [];
for (let i = 0; i < elems.length; ++i) {
if (!only_visible || is_visible(elems[i])) {
out.push(serialize_elem(elems[i]));
}
}
// Recurse into frames and add them
// ...
return {"success": true, "result": out};
};
预埋时机
位于 _WebEngineScripts
类中,该类掌管了向 WebEngineTab 中注入的各类元素(JavaScript、CSS、greasemonkey、quirks)等。对 webelem.js
的注入位于其 init
方法中:
def init(self):
"""Initialize global qutebrowser JavaScript."""
js_code = javascript.wrap_global(
'scripts',
resources.read_file('javascript/scroll.js'),
resources.read_file('javascript/webelem.js'),
resources.read_file('javascript/caret.js'),
)
# FIXME:qtwebengine what about subframes=True?
self._inject_js('js', js_code, subframes=True)
self._init_stylesheet()
self._greasemonkey.scripts_reloaded.connect(
self._inject_all_greasemonkey_scripts)
self._inject_all_greasemonkey_scripts()
self._inject_site_specific_quirks()
回调中 AbstractWebElement 的是怎么来的
在上面代码中,为什么在 JavaScript 侧执行完 find_css
,回到 Python 的 callback 回调中,就变成了 AbstractWebElement 类型?
首先,在 find_css
的 JavaScript 代码中,注意到有一个 serialize_elem
方法,他会在 JavaScript 侧获取该元素的各种属性。
再回到 Python 侧的以下实现:
def find_css(self, selector, callback, error_cb, *,
only_visible=False):
js_code = javascript.assemble('webelem', 'find_css', selector,
only_visible)
js_cb = functools.partial(self._js_cb_multiple, callback, error_cb)
self._tab.run_js_async(js_code, js_cb)
_js_cb_multiple
的实现如下:
def _js_cb_multiple(self, callback, error_cb, js_elems):
"""Handle found elements coming from JS and call the real callback.
Args:
callback: The callback to call with the found elements.
error_cb: The callback to call in case of an error.
js_elems: The elements serialized from javascript.
"""
if js_elems is None:
error_cb(webelem.Error("Unknown error while getting "
"elements"))
return
elif not js_elems['success']:
error_cb(webelem.Error(js_elems['error']))
return
elems = []
for js_elem in js_elems['result']:
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
elems.append(elem)
callback(elems)
从中可以看出:_js_cb_multiple
将 JavaScript 侧封装的元素数据,保存在 WebEngineElement 中。而 WebEngineElement 则是在 Python 侧操作的对象。
思考题:XPath 支持
qutebrowser 默认并不支持 xpath。通过上述如法炮制,不难扩展出 XPath 支持。
这里记录一个实现思路:
- 根据《Introduction to using XPath in JavaScript - XPath | MDN》一文,通过
document.evaluate
即可在 JavaScript 侧执行 XPath - 该方法返回类型为 XPathResult - Web APIs | MDN。
- 从中遍历 Node,仍然使用
serialize_elem
方法进行包装 - 在 Python 侧仍然使用 WebEngineElement 进行列表封装。
这样,便实现了通过 xpath,返回一组 WebEngineElement 序列的功能。
本文作者:Maeiee
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!